/**
 *    Copyright 2012 Geensoft S.A.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
package ar.com.greensoft.neo.dao.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;

import org.richfaces.component.SortOrder;
import org.richfaces.model.ArrangeableState;
import org.richfaces.model.FilterField;
import org.richfaces.model.SortField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import ar.com.greensoft.neo.dao.DaoException;
import ar.com.greensoft.neo.dao.GenericDao;
import ar.com.greensoft.neo.search.SearchInput;

import com.google.common.base.Strings;

/**
 * Abstract JPA Data Access Object Pattern Implementation
 * 
 * @author Jorge E. Villaverde
 * @date 08/07/2011
 * @version 1.0.0
 */
@Repository
@Transactional
public abstract class AbstractJpaDaoImpl<T> implements GenericDao<T, Long> {
	@PersistenceContext(type = PersistenceContextType.EXTENDED)
	protected EntityManager entityManager;

	protected static final Logger logger = LoggerFactory
			.getLogger(AbstractJpaDaoImpl.class);

	public T merge(T persistentObject) {
		if (logger.isTraceEnabled())
			logger.trace("Merging Entity: " + persistentObject);
		T mergeEntity = entityManager.merge(persistentObject);
		entityManager.flush();
		return mergeEntity;
	}

	public void persist(T transientObject) {
		entityManager.persist(transientObject);
	}

	public void refresh(T persistentObject) {
		entityManager.refresh(persistentObject);
	}

	public void remove(T persistentObject) {
		if (logger.isTraceEnabled())
			logger.trace("Removing " + getEntityClazz().getSimpleName()
					+ "\twith id: " + persistentObject);
		T entity = entityManager.merge(persistentObject);
		entityManager.remove(entity);
	}

	protected EntityManager getEntityManager() {
		return entityManager;
	}

	public long count() {
		if (logger.isTraceEnabled())
			logger.trace("Counting Entities in the Repository");
		return (Long) entityManager.createNamedQuery(getCountNamedQuery())
				.getSingleResult();
	}

	@Override
	public List<T> findEntries(int startPosition, int maxResult) {
		if (logger.isTraceEnabled())
			logger.trace("Returning the list of Entities from: "
					+ startPosition + " to " + (startPosition + maxResult));

		TypedQuery<T> query = createListEntityTypedQuery();

		query.setFirstResult(startPosition);
		query.setMaxResults(maxResult);

		return query.getResultList();
	}

	@Override
	public T findById(Long id) {
		if (logger.isTraceEnabled())
			logger.trace("Returning entity of type: "
					+ getEntityClazz().getSimpleName() + "\twith id: " + id);
		return getEntityManager().find(getEntityClazz(), id);
	}

	@Override
	public List<T> findAll() {
		if (logger.isTraceEnabled())
			logger.trace("Returning the list of "
					+ getEntityClazz().getSimpleName());
		TypedQuery<T> query = createListEntityTypedQuery();
		return query.getResultList();
	}

	protected TypedQuery<T> createListEntityTypedQuery() {
		return getEntityManager().createNamedQuery(getListNamedQuery(),
				getEntityClazz());
	}

	protected Collection<T> findByCriteria(CriteriaQuery<T> criteria) {
		TypedQuery<T> typedQuery = entityManager.createQuery(criteria);
		return typedQuery.getResultList();
	}

	protected T getSingleResult(TypedQuery<T> query) {
		if (logger.isTraceEnabled())
			logger.trace("Returning the Entity: "
					+ getEntityClazz().getSimpleName());
		try {
			return query.getSingleResult();
		} catch (NoResultException e) {
		} catch (NonUniqueResultException e) {
		}
		if (logger.isTraceEnabled())
			logger.trace("Entity: " + getEntityClazz().getSimpleName()
					+ " not found");
		return null;
	}

	@Override
	public List<T> findEntriesBySearchInput(SearchInput input,
			int startPosition, int maxResult) throws DaoException {
		if (logger.isTraceEnabled())
			logger.trace("Returning the list of Entities from: "
					+ startPosition + " to " + (startPosition + maxResult)
					+ " by SeachInput");

		CriteriaQuery<T> criteriaQuery = criteriaQueryFromSearchInput(input);

		TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);
		typedQuery.setFirstResult(startPosition);
		typedQuery.setMaxResults(maxResult);

		return typedQuery.getResultList();
	}

	@Override
	public long countBySearchInput(SearchInput input) throws DaoException {
		if (logger.isTraceEnabled())
			logger.trace("Counting Entities in the Repository by Search Input");

		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<Long> countCriteria = criteriaBuilder
				.createQuery(Long.class);
		Root<T> root = countCriteria.from(getEntityClazz());

		countCriteria.select(criteriaBuilder.count(root));

		Expression<Boolean> where = createWhereFromSearchInput(input, root);
		if (where != null) {
			countCriteria.where(where);
		}

		TypedQuery<Long> typedQuery = entityManager.createQuery(countCriteria);
		return typedQuery.getSingleResult();
	}

	@Override
	public List<T> findEntries(ArrangeableState state, int startPosition, int maxResult) throws DaoException {
		CriteriaQuery<T> criteriaQuery = createSelectCriteriaQuery(state);
        TypedQuery<T> query = entityManager.createQuery(criteriaQuery);
        
        if (startPosition >= 0 && maxResult > 0) {
	        query.setFirstResult(startPosition);
	        query.setMaxResults(maxResult);
        }
        
		return query.getResultList();
	}

	@Override
	public long countByArrangeableState(ArrangeableState state) throws DaoException {
        CriteriaQuery<Long> criteriaQuery = createCountCriteriaQuery(state);
        return entityManager.createQuery(criteriaQuery).getSingleResult();
	}

	protected CriteriaQuery<T> criteriaQueryFromSearchInput(SearchInput input) {
		Class<T> entityClazz = getEntityClazz();
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

		CriteriaQuery<T> criteriaQuery = criteriaBuilder
				.createQuery(entityClazz);
		Root<T> root = criteriaQuery.from(entityClazz);

		criteriaQuery.select(root);
		Expression<Boolean> where = createWhereFromSearchInput(input, root);
		if (where != null)
			criteriaQuery.where(where);

		return criteriaQuery;
	}

	private CriteriaQuery<T> createSelectCriteriaQuery(ArrangeableState state) {
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> criteriaQuery = criteriaBuilder
				.createQuery(getEntityClazz());
		Root<T> root = criteriaQuery.from(getEntityClazz());

		if (state != null) {
			List<Order> orders = createOrders(criteriaBuilder, root, state);
			if (!orders.isEmpty()) {
				criteriaQuery.orderBy(orders);
			}

			Expression<Boolean> filterCriteria = createFilterCriteria(criteriaBuilder, root, state);
			if (filterCriteria != null) {
				criteriaQuery.where(filterCriteria);
			}
		}

		return criteriaQuery;
	}

    private CriteriaQuery<Long> createCountCriteriaQuery(ArrangeableState state) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
        Root<T> root = criteriaQuery.from(getEntityClazz());
 
        Expression<Boolean> filterCriteria = createFilterCriteria(criteriaBuilder, root, state);
        if (filterCriteria != null) {
            criteriaQuery.where(filterCriteria);
        }
 
        Expression<Long> count = criteriaBuilder.count(root);
        criteriaQuery.select(count);
 
        return criteriaQuery;
    }	
	
	private List<Order> createOrders(CriteriaBuilder criteriaBuilder, Root<T> root, ArrangeableState state) {
		List<Order> orders = new ArrayList<Order>();
		List<SortField> sortFields = state.getSortFields();
		if (sortFields != null && !sortFields.isEmpty()) {
			FacesContext facesContext = FacesContext.getCurrentInstance();

			for (SortField sortField : sortFields) {
				String propertyName = (String) sortField.getSortBy().getValue(facesContext.getELContext());

				Path<Object> expression = root.get(propertyName);

				Order jpaOrder;
				SortOrder sortOrder = sortField.getSortOrder();
				if (sortOrder == SortOrder.ascending) {
					jpaOrder = criteriaBuilder.asc(expression);
				} else if (sortOrder == SortOrder.descending) {
					jpaOrder = criteriaBuilder.desc(expression);
				} else {
					throw new IllegalArgumentException(sortOrder.toString());
				}

				orders.add(jpaOrder);
			}
		}
		return orders;
	}

    private Expression<Boolean> createFilterCriteria(CriteriaBuilder criteriaBuilder, Root<T> root, ArrangeableState state) {
        Expression<Boolean> filterCriteria = null;
        List<FilterField> filterFields = state.getFilterFields();
        if (filterFields != null && !filterFields.isEmpty()) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
 
            for (FilterField filterField : filterFields) {
                String propertyName = (String) filterField.getFilterExpression().getValue(facesContext.getELContext());
                Object filterValue = filterField.getFilterValue();
 
                Expression<Boolean> predicate = createFilterCriteriaForField(propertyName, filterValue, root, criteriaBuilder, state);
 
                if (predicate == null) {
                    continue;
                }
 
                if (filterCriteria == null) {
                    filterCriteria = predicate.as(Boolean.class);
                } else {
                    filterCriteria = criteriaBuilder.and(filterCriteria, predicate.as(Boolean.class));
                }
            }
        }
        return filterCriteria;
    }
	
    protected Expression<Boolean> createFilterCriteriaForField(String propertyName, Object filterValue, Root<T> root,
            CriteriaBuilder criteriaBuilder, ArrangeableState state) {
        String stringFilterValue = (String) filterValue;
        if (Strings.isNullOrEmpty(stringFilterValue)) {
            return null;
        }
 
        stringFilterValue = stringFilterValue.toLowerCase(state.getLocale());
 
        Path<String> expression = root.get(propertyName);
        Expression<Integer> locator = criteriaBuilder.locate(criteriaBuilder.lower(expression), stringFilterValue, 1);
        return criteriaBuilder.gt(locator, 0);
    }
	
	protected abstract Class<T> getEntityClazz();

	protected abstract String getCountNamedQuery();

	protected abstract String getListNamedQuery();

	protected abstract Expression<Boolean> createWhereFromSearchInput(
			SearchInput input, Root<T> root);
}